package pl.edu.mimuw.mas.agent.behaviour.helicopter.areaassignment;

import jade.core.AID;
import jade.core.behaviours.OneShotBehaviour;
import jade.lang.acl.ACLMessage;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import pl.edu.mimuw.mas.agent.HelicopterAgent;
import pl.edu.mimuw.mas.chart.Area;
import pl.edu.mimuw.mas.exception.WrongAreaException;
import pl.edu.mimuw.mas.message.PositionMessage;
import pl.edu.mimuw.mas.util.PositionHelper;

/**
 * Zachowanie w którym zaimplementowany jest agorytm podziału panszy na
 * helikoptery. Po podzieleniu zachowanie rozsyła wyniki do agentów.
 */
public class AssignSearchAreasBehaviour extends OneShotBehaviour {
	private static final long serialVersionUID = 8917672469084052723L;
	private final Logger logger = LoggerFactory.getLogger(getClass());

	private ArrayList<Area> areaList;
	private ArrayList<AID> preAssignmentList;

	private HashSet<PositionMessage> notAssignedAgents;
	private Hashtable<AID, Area> assignment;
	
	public AssignSearchAreasBehaviour(HelicopterAgent agent, HashSet<PositionMessage> agents) {
		super(agent);
		this.notAssignedAgents = agents;
	}
	
	public void action() {
		HelicopterAgent ha = (HelicopterAgent) myAgent;
		
		int xdelta, ydelta;
		int xnum, ynum;
		
		logger.debug("{}: Dokonuje podziału planszy na {} agentów", myAgent.getName(), notAssignedAgents.size());
		
		ArrayList<Integer> xlist = new ArrayList<Integer>(2);
		xlist.add(0);
		xlist.add(ha.getChart().getColumnsCount() - 1);
		xdelta = ha.getChart().getColumnsCount();
		xnum = 1;
		
		ArrayList<Integer> ylist = new ArrayList<Integer>(2);
		ylist.add(ha.getChart().getRowsCount() - 1);
		ylist.add(0);
		ydelta = ha.getChart().getRowsCount();
		ynum = 1;
		
		// podział planszy na 2^k obszarów poszukiwań gdzie k jest min potęgą 2 
		// taką, że 2^k > ilość agentów
		int cuts = getNextPowerOf2(notAssignedAgents.size());		
		while(cuts > 0) {
			if(xdelta >= ydelta) {
				xlist = splitList(xlist);
				xdelta /= 2;
				xnum *= 2;
			} else {
				ylist = splitList(ylist);
				ydelta /= 2;
				ynum *= 2;
			}
			cuts--;
		}
		
		// sortowanie i tworzenie obiektów typu Area
		areaList = new ArrayList<Area>((xlist.size() - 1) * (ylist.size() - 1));				
		CreateAreas(xlist, ylist, areaList);
		
		if (logger.isTraceEnabled()) {
			logger.trace("{}: wygenerowałem następujące obszary poszukiwań");
			for (Area a : areaList) {
				int[] ra = a.getArea();
				logger.trace("(" + ra[0] + ", " + ra[1] + ", " + ra[2] + "," + ra[3] + ")");
			}
		}
		
		// "faza I" (agent -> obszar)
		int freeAreas = (xlist.size() - 1) * (ylist.size() - 1);
		preAssignmentList = new ArrayList<AID>(freeAreas);
		for(Area a : areaList) {			
			if(notAssignedAgents.size() == 0) {
				break;
			}
			
			int[] realArea = a.getArea();
			preAssignmentList.add(nearest((realArea[Area.X1] - realArea[Area.X0]) / 2, 
					(realArea[Area.Y1] - realArea[Area.Y0]) / 2));
			freeAreas--;
		}

		// "faza II" (obszar -> agent)
		try {
			assignment = new Hashtable<AID, Area>(preAssignmentList.size());
			for (int row = 0; row < ynum; row++) {
				int rass = agentsWellAssignedInRow(row, ynum, xnum, freeAreas);

				int col = 0;
				int agent = 0;
				while (col < xnum) {
					if (col < rass) {
						assignment.put(preAssignmentList.get(agent * ynum + row), areaList.get(col * ynum + row));
						col++;
						agent++;
					} else {
						Area exarea = Area.join(areaList.get(col * ynum + row), areaList.get((col + 1) * ynum + row));
						assignment.put(preAssignmentList.get(agent * ynum + row), exarea);
						col += 2;
						agent++;
					}
				}
			}
		} catch (WrongAreaException e) {
			logger.error("{}: Niepoprawna definicja obszaru podczas złączania",
					myAgent.getName());
			myAgent.doDelete();
			return;
		}
		
		// rozgłaszanie 
		informAgents();
	}
	
	private void informAgents() {
		for(AID aid : assignment.keySet()) {
			try {
				ACLMessage msg = new ACLMessage(ACLMessage.INFORM);
				msg.setConversationId(HelicopterAgent.HELICOPTER_ASSIGN_SEARCH_AREA_CONV);
				msg.addReceiver(aid);
				msg.setContentObject(assignment.get(aid));
				
				logger.trace("{}: Wysyłam przypisany obszar poszukiwań do agenta {}", myAgent.getName(), aid.getName());
				
				myAgent.send(msg);
			} catch (IOException e) {
			}
		}
	}
	
	private int agentsWellAssignedInRow(int row, int rowtotal, int coltotal, int freeAreas) {
		int base = freeAreas / rowtotal;
		int offset = freeAreas % rowtotal;
		
		int res = (row >= rowtotal - offset) ? base + 1 : base;
		
		return coltotal - 2 * res;
	}
	
	private void CreateAreas(ArrayList<Integer> xlist, ArrayList<Integer> ylist, ArrayList<Area> areas) {
		try {
			for (int xp = 0; xp < xlist.size() - 1; xp++) {
				for (int yp = 0; yp < ylist.size() - 1; yp++) {
					areas.add(new Area(xlist.get(xp), ylist.get(yp + 1), xlist.get(xp + 1), ylist.get(yp)));
				}
			}
		} catch (WrongAreaException e) {
			logger.error("{}: Niepoprawna definicja obszaru", myAgent.getName());
		}
	}
	
	private AID nearest(int x, int y) {
		float minDist = Float.MAX_VALUE;
		PositionMessage minAgent = null;

		for(PositionMessage pm : notAssignedAgents) {
			float tmpDist = PositionHelper.getDistance(x, y, pm.x, pm.y); 
			if(tmpDist < minDist) {
				minDist = tmpDist;
				minAgent = pm;
			}
		}
		
		notAssignedAgents.remove(minAgent);
		return minAgent.getAID();
	}
	
	private ArrayList<Integer> splitList(ArrayList<Integer> in) {
		ArrayList<Integer> out = new ArrayList<Integer>(in.size() * 2); 
		
		Iterator<Integer> it = in.iterator();
		int b = it.next();
		out.add(b);
		
		while(it.hasNext()) {			
			int e = it.next();
						
			out.add((b + e) / 2);
			out.add(e);
			
			b = e;
		}
		
		return out;		
	}
	
	private int getNextPowerOf2(int n) {
		int pow = 0;
		int nn = 1;
		
		while(nn < n) {
			nn *= 2;
			pow++;
		}
		
		return pow;
	}
}
